package edu.buaa.act.model.tsc;

import edu.buaa.act.model.Query;
import edu.buaa.act.model.Trajectory;
import edu.buaa.act.model.tsc.MyMapMatching.TrajectoryRoadEntry;

import java.io.*;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Created by sjh on 2017/4/1.
 *
 * this class is a path (or route) travel time estimation model
 * called: Trajectory-based Simple Concatenation(TSC) model
 * TSC estimates the travel time of each road segment individually based
 * on the trajectories passing the road segment in the most recent
 * time slot. If a road segment is not covered by any recent
 * trajectory, TSC uses the average historical travel time instead. The
 * travel time of a path is then a summation of that of each road
 * segment.
 *
 * note: 1 timeSlot is 30 minutes.
 *
 * model input:
 * 1. History trajectories (space separated column)
 * driverID roadID timeSlot timeSpend(on this road, in seconds)
 * 2. Query path (route) (roadID list)
 * roadID must connected from start point to end point.
 * 3. Departure time of queryEdgeTravelTime path (in timeSlot)
 *
 * model output:
 * travel time of the queryEdgeTravelTime path. (in seconds)
 *
 * how to use:
 * step 1: read history trajectories and build index (for faster access)
 * step 2: queryEdgeTravelTime path at specified time and get result.
 */
public class TrajectorySimpleConcat
{
    private Map<Integer, Map<Integer, List<Integer>>> today = new HashMap<>(); //edgeId -- timeSlot -- travelTime
    private Map<Integer, Map<Integer, List<Integer>>> history = new HashMap<>(); //edgeId -- timeSlot -- travelTime

    /**
     *
     * @param query
     * @return travel time in seconds
     * @throws EdgeNotFoundException
     */
    public int queryPathTravelTime(Query query) throws EdgeNotFoundException
    {
        List<Integer> path = query.getRoadIdList();
        long departureTimestamp = query.getDepartureTimestamp();
        long curTimestamp = departureTimestamp;

        for(Integer edgeId : path){
            int seconds;
            int currentTimeSlot = timestamp2TimeSlot(departureTimestamp);
            try{
                seconds = queryEdgeTravelTime(today, edgeId, currentTimeSlot, false);
            }catch (EdgeNotFoundException e) {
                seconds = queryEdgeTravelTime(history, edgeId, currentTimeSlot, true);
            }
            curTimestamp += seconds*1000;
        }

        return (int) ((curTimestamp-departureTimestamp)/1000);
    }

    private int queryEdgeTravelTime(Map<Integer, Map<Integer, List<Integer>>> db, int edgeId, int currentTimeSlot, boolean allowSearchFuture) throws EdgeNotFoundException
    {
        if(db.containsKey(edgeId)){
            Map<Integer, List<Integer>> edge = db.get(edgeId);
            if(edge.containsKey(currentTimeSlot)){
                try {
                    return avgTime(edge.get(currentTimeSlot));
                }catch (EdgeNotFoundException ignore){
                    // return avg travel time of the hit time slot.
                    // if not found then continue searching nearest.
                }
            }
            for(int i=1; i<48; i++){
                try {
                    int recentTimeSlot = currentTimeSlot + i;
                    if(allowSearchFuture && (recentTimeSlot<48) && edge.containsKey(recentTimeSlot)){
                        return avgTime(edge.get(recentTimeSlot));
                    }

                    recentTimeSlot = currentTimeSlot - i;
                    if((recentTimeSlot>=0) && edge.containsKey(recentTimeSlot)){
                        return avgTime(edge.get(recentTimeSlot));
                    }
                }catch (EdgeNotFoundException ignore){
                    // return nearest time.
                }
            }
            throw new EdgeNotFoundException("edgeID "+edgeId+" found, but no travel time in database!");
        }else{
            throw new EdgeNotFoundException("edgeID "+edgeId+" not in database");
        }
    }

    private static long timeStrToStamp(String time) throws ParseException
    {
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        df.setTimeZone(TimeZone.getTimeZone("GMT+8:00")); // "UTC"
        return df.parse(time).getTime();
    }

    private int timestamp2TimeSlot(long timestamp)
    {
        Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT+8:00"));
        c.setTimeInMillis(timestamp);
        int timeSlot = c.get(Calendar.HOUR_OF_DAY)*2;
        if(c.get(Calendar.MINUTE)>=30){
            timeSlot++;
        }
        return timeSlot;
    }

    private int avgTime(List<Integer> timeList) throws EdgeNotFoundException
    {
        int sum=0;
        boolean looped = false;
        for(Integer t:timeList){
            if(t!=-1) {
                sum += t;
                looped=true;
            }
        }
        if(!looped){
            throw new EdgeNotFoundException("only query edge here");
        }else {
            return sum / timeList.size();
        }
    }


    /**
     * the same as #validateInternalTraj(Query query) but exclude the traj from db.
     * @param traj exclude this traj from db.
     * @return travel time in seconds
     */
    public int validateInternalTraj(Trajectory traj) throws EdgeNotFoundException
    {
        maskTraj(traj);
        int travelTime = queryPathTravelTime(traj.toQuery());
        recoverTraj(traj);
        traj.setEstimateTravelTime(travelTime);
        return travelTime;
    }


    /**
     * the same as #validateInternalTraj(Query query) but exclude the traj from db.
     * @param traj exclude this traj from db.
     * @return travel time in seconds
     */
    public int validateExternalTraj(Trajectory traj) throws EdgeNotFoundException
    {
        int travelTime = queryPathTravelTime(traj.toQuery());
        traj.setEstimateTravelTime(travelTime);
        return travelTime;
    }




//    public void readTraj(File inputHistoryTraj ) throws IOException
//    {
//        BufferedReader ir = new BufferedReader(new FileReader(inputHistoryTraj));
//        String lineTxt = null;
//        boolean firstLoop=true;
//        long lastCarID=0;
//        while((lineTxt = ir.readLine()) != null)
//        {
//            String[] arr = lineTxt.split(" ");
//            long carID = Long.parseLong(arr[0]);
//            int timeSlot = Integer.parseInt(arr[1]);
//            float latitude = Float.parseFloat(arr[2]);
//            float longitude = Float.parseFloat(arr[3]);
//
//            if(firstLoop)
//            {
//                lastCarID = carID;
//                firstLoop = false;
//            }
//            else
//            {
//                if(carID!=lastCarID) break;
//
//
//            }
//        }
//        ir.close();
//    }

    /**
     * 'hide' this traj from queries temporary
     * @param traj
     */
    private void maskTraj(Trajectory traj)
    {
        for(TrajectoryRoadEntry road: traj.getRoads()){
            Map<Integer, List<Integer>> time = today.get(road.edgeID);
            List<Integer> list = time.get(road.timeSlot);
            int index = list.lastIndexOf(road.travelTime);
            list.set(index, -1);
        }
    }

    /**
     * undo the mask operation.
     * @param traj  must be the same traj. or may throw NullPointerException.
     */
    private void recoverTraj(Trajectory traj)
    {
        for(TrajectoryRoadEntry road: traj.getRoads()){
            Map<Integer, List<Integer>> time = today.get(road.edgeID);
            List<Integer> list = time.get(road.timeSlot);
            int index = list.lastIndexOf(-1);
            list.set(index, road.travelTime);
        }
    }


    public void readTrajInc(Trajectory traj )
    {
        for(TrajectoryRoadEntry road: traj.getRoads()){
            if(!today.containsKey(road.edgeID)){
                today.put(road.edgeID, new HashMap<Integer, List<Integer>>());
            }
            Map<Integer, List<Integer>> time = today.get(road.edgeID);
            if(!time.containsKey(road.timeSlot)){
                time.put(road.timeSlot, new ArrayList<Integer>());
            }
            List<Integer> list = time.get(road.timeSlot);
            list.add(road.travelTime);
        }
    }

    public void readHistoryTrajInc(Trajectory traj )
    {
        for(TrajectoryRoadEntry road: traj.getRoads()){
            if(!history.containsKey(road.edgeID)){
                history.put(road.edgeID, new HashMap<Integer, List<Integer>>());
            }
            Map<Integer, List<Integer>> time = history.get(road.edgeID);
            if(!time.containsKey(road.timeSlot)){
                time.put(road.timeSlot, new ArrayList<Integer>());
            }
            List<Integer> list = time.get(road.timeSlot);
            list.add(road.travelTime);
        }
    }

    public void save2File(File backupFile) throws IOException
    {
        if(backupFile!=null){
            Writer ow = new BufferedWriter(new FileWriter(backupFile));
            writeDB(history, ow);
            ow.write("# above is 'history', below is 'today'.\n");
            writeDB(today, ow);
            ow.close();
//            ObjectOutputStream oos = null;
//            try{
//                oos = new ObjectOutputStream(new FileOutputStream(this.backupFile));
//                oos.writeObject(today);
//                oos.writeObject(history);
//            } finally {
//                if(oos  != null){
//                    oos.close();
//                }
//            }
        }else{
            throw new RuntimeException("backup file is null");
        }
    }

    private void writeDB(Map<Integer, Map<Integer, List<Integer>>> db, Writer w) throws IOException
    {
        for(Map.Entry<Integer, Map<Integer, List<Integer>>> edge: db.entrySet()){
            Map<Integer, List<Integer>> v = edge.getValue();
            for(Map.Entry<Integer, List<Integer>> timeSlot: v.entrySet()){
                List<Integer> l = timeSlot.getValue();
                Collections.sort(l);
                w.write(edge.getKey()+" "+timeSlot.getKey());
                for (Integer aL : l) {
                    w.write(" " + aL);
                }
                w.write('\n');
            }
        }
    }

    public void readDB(File backupFile) throws IOException, ClassNotFoundException
    {
        BufferedReader r = new BufferedReader(new FileReader(backupFile));
        String line;
        Map<Integer, Map<Integer, List<Integer>>> db = this.history;
        while((line=r.readLine())!=null){
            String[] arr = line.split(" ");
            if(arr[0].startsWith("#")){
                db = this.today;
            }else{
                int edgeID = Integer.parseInt(arr[0]);
                int timeSlot = Integer.parseInt(arr[1]);
                if(!db.containsKey(edgeID)){
                    db.put(edgeID, new HashMap<Integer, List<Integer>>());
                }
                Map<Integer, List<Integer>> time = history.get(edgeID);
                if(!time.containsKey(timeSlot)){
                    time.put(timeSlot, new ArrayList<Integer>());
                }
                List<Integer> list = time.get(timeSlot);
                for(int i=2; i<arr.length; i++){
                    list.add(Integer.parseInt(arr[i]));
                }
            }
        }
        r.close();

    }

    public int getTodayRoadCount()
    {
        return today.size();
    }

    public int getHistoryRoadCount(){
        return history.size();
    }

    public int getMergedRoadCount() {
        Set<Integer> set = new HashSet<>();
        set.addAll(today.keySet());
        set.addAll(history.keySet());
        return set.size();
    }

    public static class EdgeNotFoundException extends Exception{
        public EdgeNotFoundException(String msg){super(msg);}
    }
}

//        ObjectInputStream objectinputstream = null;
//        try {
//            objectinputstream = new ObjectInputStream(new FileInputStream(this.backupFile));
//            this.today = (Map<Integer, Map<Integer, List<Integer>>>) objectinputstream.readObject();
//            this.history = (Map<Integer, Map<Integer, List<Integer>>>) objectinputstream.readObject();
//        }finally {
//            if(objectinputstream != null){
//                objectinputstream .close();
//            }
//        }


//    public void verify() throws IOException, ClassNotFoundException
//    {
//        Map<Integer, Map<Integer, List<Integer>>> t;
//        Map<Integer, Map<Integer, List<Integer>>> h;
//        ObjectInputStream objectinputstream = null;
//        try {
//            objectinputstream = new ObjectInputStream(new FileInputStream(this.backupFile));
//            t = (Map<Integer, Map<Integer, List<Integer>>>) objectinputstream.readObject();
//            h = (Map<Integer, Map<Integer, List<Integer>>>) objectinputstream.readObject();
//            verify(t, today);
//            System.out.println("----------------");
//            verify(h, history);
//        }finally {
//            if(objectinputstream != null){
//                objectinputstream .close();
//            }
//        }
//
//    }
//
//    private void verify(Map<Integer, Map<Integer, List<Integer>>> t, Map<Integer, Map<Integer, List<Integer>>> db)
//    {
//        for(Map.Entry<Integer, Map<Integer, List<Integer>>> e: db.entrySet()){
//            if(t.containsKey(e.getKey())){
//                Map<Integer, List<Integer>> vt = t.get(e.getKey());
//                Map<Integer, List<Integer>> v = e.getValue();
//                for(Map.Entry<Integer, List<Integer>> e0: v.entrySet()){
//                    if(vt.containsKey(e0.getKey())){
//                        List<Integer> lt = vt.get(e0.getKey());
//                        List<Integer> l = e0.getValue();
//                        Collections.sort(lt);
//                        Collections.sort(l);
//                        for(int i=0; i<l.size(); i++){
//                            if(lt.get(i)!=l.get(i)){
//                                throw new RuntimeException("element not found.");
//                            }
//                        }
//                    }
//                }
//            }else{
//                throw new RuntimeException("edge not found.");
//            }
//        }
//    }